Circuit Breaker
contents
"회복력 있는(Resilient)" 시스템을 구축하기 위해 가장 중요한 패턴 중 하나인 서킷 브레이커(Circuit Breaker, 회로 차단기) 패턴에 대해 알아보겠습니다.
이 패턴이 없다면, 단 하나의 마이크로서비스 장애가 전체 아키텍처를 무너뜨릴 수 있습니다(도미노 효과).
다음은 서킷 브레이커 패턴의 핵심 개념, 내부 작동 원리, 그리고 Spring Boot에서의 구현 방법에 대한 상세 분석입니다.
1. 핵심 개념: "전기 퓨즈 (The Electrical Fuse)"
여러분의 집을 상상해 보세요. 만약 전자레인지가 합선되어 과도한 전류가 흐른다면 어떻게 될까요?
- 퓨즈가 없다면: 벽 속의 전선이 녹아내리고, 집 전체에 화재가 발생할 것입니다.
- 퓨즈(차단기)가 있다면: 퓨즈가 끊어지며(Trips/Opens) 부엌으로 가는 전기를 차단합니다. 덕분에 집안의 다른 곳(전등, TV 등)은 안전하게 유지됩니다.
마이크로서비스 세계에서는:
Service A 가 Service B 를 호출했는데, Service B 가 죽었거나 매우 느리다고 가정해 봅시다.
- 서킷 브레이커가 없다면: Service A 는 Service B 의 응답을 하염없이 기다립니다. Service A 의 스레드(Thread)들이 대기 상태로 묶여버립니다. 결국 Service A 의 스레드가 고갈되어 Service A 마저 죽게 됩니다. 이 현상이 시스템 전체로 퍼져 나갑니다.
- 서킷 브레이커가 있다면: 몇 번 실패가 반복되면 차단기가 "작동(Trip)"합니다. Service A 는 즉시 Service B 호출을 멈추고 미리 정의된 에러나 기본값(Fallback)을 반환합니다. Service A 는 살아남습니다.
2. 내부 상태 머신 (Internal State Machine)
서킷 브레이커는 세 가지 주요 상태를 가진 유한 상태 기계(Finite State Machine) 입니다.
A. CLOSED (닫힘 - 정상 상태)
- 상태: 모든 것이 정상적으로 작동합니다.
- 동작: 외부 서비스로의 요청 통과를 허용합니다.
- 모니터링: 브레이커가 성공 및 실패 비율을 계속 계산합니다.
B. OPEN (열림 - 장애 상태)
- 발동 조건: 실패율이 임계치를 초과하면(예: 최근 10번 요청 중 50% 실패), 브레이커가 OPEN 상태로 바뀝니다.
- 동작: 모든 요청을 즉시 차단합니다. 외부 서비스로 요청을 아예 보내지 않습니다.
- 결과: 사용자는 기다림 없이 즉시 에러를 받거나 "폴백(Fallback)" 응답(예: 캐시된 데이터)을 받습니다.
C. HALF-OPEN (반열림 - 테스트 상태)
- 발동 조건: 설정된 "대기 시간(Wait Duration)"(예: 10초)이 지나면, 브레이커는 조심스럽게 HALF-OPEN 상태로 변경됩니다.
- 동작: 외부 서비스가 복구되었는지 확인하기 위해 제한된 수의 요청(예: 3개)만 통과시킵니다.
- 결과:
- 3개의 요청이 모두 성공하면: 브레이커는 다시 CLOSED로 돌아갑니다 (시스템 복구됨).
- 단 하나라도 실패하면: 브레이커는 다시 OPEN 상태로 돌아갑니다 (아직 고장 남).
3. Spring Boot에서 구현하기 (Resilience4j)
과거에는 Netflix Hystrix(현재 개발 중단됨)를 썼지만, 오늘날의 업계 표준은 Resilience4j입니다.
1단계: 의존성 추가
org.springframework.cloud
spring-cloud-starter-circuitbreaker-resilience4j
org.springframework.boot
spring-boot-starter-aop
2단계: 설정 (application.yml)
여기서 민감도를 설정합니다.
resilience4j:
circuitbreaker:
instances:
paymentService: # 브레이커 이름
registerHealthIndicator: true
slidingWindowSize: 10 # 최근 10개의 요청을 기준으로 판단
minimumNumberOfCalls: 5 # 최소 5번의 호출은 있어야 계산 시작
permittedNumberOfCallsInHalfOpenState: 3 # HALF-OPEN 상태에서 허용할 요청 수
waitDurationInOpenState: 10s # OPEN 상태에서 10초 대기 후 HALF-OPEN으로 전환
failureRateThreshold: 50 # 실패율이 50% 넘으면 OPEN
3단계: 서비스 코드에 적용
@CircuitBreaker 애노테이션을 사용합니다.
@Service
public class OrderService {
private final RestTemplate restTemplate;
// 'name'은 yaml 설정의 이름과 일치해야 합니다
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallbackPayment")
public String processOrder(Order order) {
// 이 호출이 50% 확률로 실패하면 회로가 열립니다(Open)
return restTemplate.getForObject("http://payment-service/pay", String.class);
}
// 폴백 메서드 (FALLBACK METHOD)
// 원본 메서드와 시그니처가 같아야 하며, 마지막 파라미터로 예외(Throwable)를 받습니다
public String fallbackPayment(Order order, Throwable t) {
return "결제 서비스가 혼잡합니다. 잠시 후 다시 시도해주세요. (캐시된 응답 반환)";
}
}
4. 심화 개념: "슬라이딩 윈도우 (Sliding Window)"
실패를 어떻게 카운트할까요? Resilience4j는 두 가지 타입을 사용합니다.
- 횟수 기반 (Count-Based - 기본값): "최근 100개의 요청을 봐라."
- 단순하고 이해하기 쉽습니다.
- 시간 기반 (Time-Based): "최근 10초 동안의 모든 요청을 봐라."
- 트래픽이 매우 많은 시스템에서 "최근 100개"가 0.001초 만에 지나가 버릴 때 유용합니다.
5. 서킷 브레이커 vs. 재시도 (Retry)
학생들이 이 둘을 자주 혼동합니다.
- 재시도 (Retry): "서비스가 잠깐 삐끗했나 보네. 즉시 다시 시도해보자." (일시적인 네트워크 깜빡임에 좋음).
- 서킷 브레이커 (Circuit Breaker): "서비스가 확실히 죽었어. 더 이상 때리지 말고 멈춰." (지속적인 장애에 좋음).
모범 사례 (Best Practice): 둘을 결합하세요! 3번 재시도(Retry)를 합니다. 3번 다 실패하면, 그때 서킷 브레이커 입장에서 1번의 실패로 카운트합니다.
면접용 요약 체크리스트
- 목적: 연쇄적인 장애(Cascading Failures)를 막고 스레드 자원을 보호한다.
- 라이브러리: Resilience4j (Hystrix는 이제 안 씀).
- 상태: Closed (정상/통과), Open (차단), Half-Open (간보기/테스트).
- 폴백 (Fallback): 회로가 열렸을 때 실행되는 플랜 B (캐시 반환, 기본값 반환, 또는 커스텀 에러).
references